flutter - 动画实现

flutter - 动画实现

前言

动画是客户端开发中非常重要的一个方面,良好的动画设计能带给用户非常好的交互体。flutter 提供了一个非常灵活的动画框架,可以非常轻松地实现各种动效。这篇文章就将带你深入了解。

从 AnimationController.forward 方法开始

AnimationController.forward 是最常用的启动动画的方法,该方法调用后 AnimationController.value 值会在持续时间内持续变化,我们可以从这个方法开始分析整个 flutter 的动画实现原理

AnimationController.forward

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TickerFuture forward({ double from }) {
assert(() {
if (duration == null) {
throw new FlutterError(
'AnimationController.forward() called with no default Duration.\n'
'The "duration" property should be set, either in the constructor or later, before '
'calling the forward() function.'
);
}
return true;
}());
_direction = _AnimationDirection.forward;
if (from != null)
value = from;
return _animateToInternal(upperBound);
}

确定了动画的方向,以及可能的动画起始值,返回值是一个 TickerFuture 类型的异步任务

AnimationController._animateToInternal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
TickerFuture _animateToInternal(double target, { Duration duration, Curve curve: Curves.linear }) {
Duration simulationDuration = duration;
if (simulationDuration == null) {
assert(() {
if (this.duration == null) {
throw new FlutterError(
'AnimationController.animateTo() called with no explicit Duration and no default Duration.\n'
'Either the "duration" argument to the animateTo() method should be provided, or the '
'"duration" property should be set, either in the constructor or later, before '
'calling the animateTo() function.'
);
}
return true;
}());
final double range = upperBound - lowerBound;
final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
simulationDuration = this.duration * remainingFraction;
} else if (target == value) {
// Already at target, don't animate.
simulationDuration = Duration.zero;
}
stop();
if (simulationDuration == Duration.zero) {
if (value != target) {
_value = target.clamp(lowerBound, upperBound);
notifyListeners();
}
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
_checkStatusChanged();
return new TickerFuture.complete();
}
assert(simulationDuration > Duration.zero);
assert(!isAnimating);
return _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve));
}

会根据目标值 target 和起始值 _value 计算整个动画的持续时间。先通过 stop 方法暂停可以存在的动画。如果计算出的持续时间为 0,意味着不需要进行动画,回调一些必要的 Listener 后结束,否则就开始启动动画,调用 _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve)) 方法。

AnimationController._startSimulation

1
2
3
4
5
6
7
8
9
10
11
12
13
TickerFuture _startSimulation(Simulation simulation) {
assert(simulation != null);
assert(!isAnimating);
_simulation = simulation;
_lastElapsedDuration = Duration.zero;
_value = simulation.x(0.0).clamp(lowerBound, upperBound);
final Future<Null> result = _ticker.start();
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward :
AnimationStatus.reverse;
_checkStatusChanged();
return result;
}

Simulation 是用来模拟在一维空间中一个物体受力改变的情况,通过 x 函数可以获取给定时间物体的位置,dx 函数可以获取给定时间物体的速度,isDone 函数获取给定时间是否完成。通过自定义各种各样的 simulation 可以模拟出真实场景下的动画效果。接下去我们可以看到动画运行的关键 _ticker.start()

AnimationController 的构造函数中 TickerProvider 是一个必要的参数,通过 TickerProvider.createTicker(TickerCallback onTick) 方法可以生成 _ticker(Ticker) 对象,那么这个 _ticker 到底是用来干嘛的

Ticker 是每一帧用来调用动画回调的,它由 SchedulerBinding 驱动,什么意思呢,我们可以从 start 方法深入了解

Ticker.start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TickerFuture start() {
assert(() {
if (isActive) {
throw new FlutterError(
'A ticker was started twice.\n'
'A ticker that is already active cannot be started again without first stopping it.\n'
'The affected ticker was: ${ toString(debugIncludeStack: true) }'
);
}
return true;
}());
assert(_startTime == null);
_future = new TickerFuture._();
if (shouldScheduleTick) {
scheduleTick();
}
if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _future;
}

回调添加在 scheduleTick 方法进行,随后记录了起始时间

Ticker.scheduleTick

1
2
3
4
5
void scheduleTick({ bool rescheduling: false }) {
assert(!scheduled);
assert(shouldScheduleTick);
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}

这时 Ticker 会向 SchedulerBinding 通过 scheduleFrameCallback 注册帧回调,回调方法为 _tick,记录当前帧的回调主键 _animationId,可用于取消动画时注销回调

在 ScheduleBinding 初始化的工作中,ui.window.onBeginFrame = _handleBeginFrame,这里将从 flutter 引擎传来的每一帧开时回调传递给 _handleBeginFrame 方法,在该方法中会回调注册过的动画回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
// TRANSIENT FRAME CALLBACKS
Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
_schedulerPhase = SchedulerPhase.transientCallbacks;
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
_transientCallbacks = <int, _FrameCallbackEntry>{};
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
});
_removedIds.clear();
} finally {
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}

这里会遍历 callbacks,排除掉已经移除的回调方法,依次触发回调方法,回到 Ticker._tick

Tick._tick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void _tick(Duration timeStamp) {
assert(isTicking);
assert(scheduled);
_animationId = null;

_startTime ??= timeStamp;

_onTick(timeStamp - _startTime);

// The onTick callback may have scheduled another tick already, for
// example by calling stop then start again.
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}

这里就会回调 AnimationController._tick,但是这里传递的 time 就是已经减去起始值的持续时间了,最后还会根据是否需要注册下一帧的回调

AnimationController._tick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed;
final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
assert(elapsedInSeconds >= 0.0);
_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
if (_simulation.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
stop(canceled: false);
}
notifyListeners();
_checkStatusChanged();
}

根据 elapsed 和 _simulation 计算当前的 value 值,当判断动画结束时就通过 stop 方法调用 ticker.stop 结束回调,具体操作也就是根据 _animationId 去移除回调方法,然后结束代表动画任务的 TickerFuture,重置参数,unscheduleTick

对于其他的方法操作 [reverse]、[animateTo]、[animateWith]、[fling] 和 [repeat], 都是类似的原理,可能具体实施有些区别或者 Simulation 不同而已